
package com.tendory.gpssim.service;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

import com.tendory.common.base.RxBus;
import com.tendory.gpssim.DatabaseHelper;
import com.tendory.gpssim.NetworkManager;
import com.tendory.gpssim.Position;
import com.tendory.gpssim.PositionProvider;
import com.tendory.gpssim.R;
import com.tendory.gpssim.activity.MainFragment;
import com.tendory.gpssim.activity.StatusActivity;
import com.tendory.gpssim.evt.EvtIpConnected;
import com.tendory.gpssim.evt.EvtPosSent;
import com.tendory.gpssim.evt.EvtSendSimLoc;
import com.tendory.gpssim.evt.EvtStChanged;
import com.tendory.gpssim.evt.EvtStartPos;
import com.tendory.gpssim.evt.EvtStopPos;
import com.tendory.gpssim.service.jt808.FrameDecoderHandler;
import com.tendory.gpssim.service.jt808.MsgInHandler;
import com.tendory.gpssim.service.jt808.MsgOutHandler;
import com.tendory.jt808.msg.AuthenticateRequest;
import com.tendory.jt808.msg.CommandReply;
import com.tendory.jt808.msg.HeartbeatMessage;
import com.tendory.jt808.msg.LocationMessage;
import com.tendory.jt808.msg.Message;
import com.tendory.jt808.msg.MessageBuilder;
import com.tendory.jt808.msg.Packet;
import com.tendory.jt808.msg.RegisterReply;
import com.tendory.jt808.msg.RegisterRequest;
import com.tendory.jt808.msg.ServerGenericReply;
import com.tendory.jt808.msg.ServerGenericRequest;
import com.tendory.jt808.util.ArrayUtils;
import com.tendory.rxlocation.GPSUtil;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.text.SimpleDateFormat;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TrackingController implements NetworkManager.NetworkHandler {

    private static final String TAG = TrackingController.class.getSimpleName();
    private static final int RETRY_DELAY = 30 * 1000;
    private static final int WAKE_LOCK_TIMEOUT = 120 * 1000;

    private boolean isOnline;
    private boolean isWaiting;

    private Context context;
    private Handler handler;
    private SharedPreferences preferences;

    private String url;
    private boolean buffer;

    private PositionProvider positionProvider;
    private DatabaseHelper databaseHelper;
    private NetworkManager networkManager;

    private PowerManager.WakeLock wakeLock;
    private String mHost;
    private int mPort;
    private String mAuthCode;

    private void lock() {
        wakeLock.acquire(WAKE_LOCK_TIMEOUT);
    }

    private void unlock() {
        if (wakeLock.isHeld()) {
            wakeLock.release();
        }
    }


    public TrackingController(Context context) {
        this.context = context;
        handler = new Handler();
        preferences = PreferenceManager.getDefaultSharedPreferences(context);
        positionProvider = new PositionProvider(context, this::sendPos);
        databaseHelper = new DatabaseHelper(context);
        networkManager = new NetworkManager(context, this);
        isOnline = networkManager.isOnline();

        url = preferences.getString(MainFragment.KEY_URL, context.getString(R.string.settings_url_default_value));
        buffer = preferences.getBoolean(MainFragment.KEY_BUFFER, true);
        String[] us = url.split(":");
        if (us.length == 2) {
            try {
                int port = Integer.parseInt(us[1]);
                if (port == -1 || (port > 0 && port <= 65535)) {
                    mHost = us[0];
                    mPort = port;
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
            }
        }
        PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
    }

    public void start() {
        EventBus.getDefault().register(this);
//        if (isOnline) {
            stateMerchine();
//        }
        networkManager.start();
        if (preferences.getBoolean(MainFragment.KEY_ENABLE_POS, false)) {
            startSendLocation();
        }
    }

    public void stop() {
        networkManager.stop();
        try {
            positionProvider.stopUpdates();
        } catch (SecurityException e) {
            Log.w(TAG, e);
        }
        handler.removeCallbacksAndMessages(null);
        handler.removeCallbacks(reconnRun);

        stopHeart();
        stopNetty();
        EventBus.getDefault().unregister(this);
    }

    @Override
    public void onNetworkUpdate(boolean isOnline) {
        int message = isOnline ? R.string.status_network_online : R.string.status_network_offline;
        StatusActivity.addMessage(context.getString(message));
//        if (!this.isOnline && isOnline) {
//            read();
//        }
        this.isOnline = isOnline;
        if (!isOnline) {
            this.isAuthenticate = false;
        }
    }

    //
    // State transition examples:
    //
    // write -> read -> send -> delete -> read
    //
    // read -> send -> retry -> read -> send
    //

    private void log(String action, Position position) {
        if (position != null) {
            action += " (" +
                    "id:" + position.getId() +
                    " time:" + position.getTime().getTime() / 1000 +
                    " lat:" + position.getLatitude() +
                    " lon:" + position.getLongitude() + ")";
        }
        Log.d(TAG, action);
    }

    private void write(Position position) {
        log("write", position);
        lock();
        databaseHelper.insertPositionAsync(position, (success, result) -> {
            if (success) {
                if (isOnline && isWaiting) {
                    read();
                    isWaiting = false;
                }
            }
            unlock();
        });
    }

    private void read() {
        log("read", null);
        lock();
        databaseHelper.selectPositionAsync((success, result) -> {
            if (success) {
                if (result != null) {
                    if (result.getDeviceId().equals(preferences.getString(MainFragment.KEY_DEVICE, null))) {
                        send(result);
                    } else {
                        delete(result);
                    }
                    handler.postDelayed(this::read, 20);
                } else {
                    isWaiting = true;
                }
            } else {
                retry();
            }
            unlock();
        });
    }

    private void delete(Position position) {
        log("delete", position);
        lock();
        databaseHelper.deletePositionAsync(position.getId(), (success, result) -> {
            if (success) {
                read();
            } else {
                retry();
            }
            unlock();
        });
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvtStChanged(EvtStChanged event) {
//        send();
        positionProvider.locImmediately();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(Message msg) {
        switch (msg.getId()) {
            case RegisterReply.ID: {
                RegisterReply reply = new RegisterReply.Builder(msg).build();
                switch (reply.getResult()) {
                    case RegisterReply.RESULT_OK:
                        mAuthCode = reply.getAuthCode();
                        StatusActivity.addMessage("authCode:" + mAuthCode);
//                    mPrefs.edit().putString(ClientConstants.PREF_KEY_AUTH_CODE, mAuthCode).commit();
                        state = ST_REGIST;
                        stateMerchine();

                        Log.i(TAG, "Client registered SUCCESS !!!");
                        break;
                    case RegisterReply.RESULT_VEH_NOT_FOUND:
                        Log.w(TAG, "Registration failed - vehicle not found");
                        break;
                    case RegisterReply.RESULT_VEH_REGISTERED:
                        Log.w(TAG, "Registration failed - vehicle registered");
                        break;
                    case RegisterReply.RESULT_CLT_NOT_FOUND:
                        Log.w(TAG, "Registration failed - client not found");
                        break;
                    case RegisterReply.RESULT_CLT_REGISTERED:
                        Log.w(TAG, "Registration failed - client registered");
                        break;
                    default:
                        Log.e(TAG, "Unknown registration result");
                }
                break;
            }
            case ServerGenericReply.ID: {
                ServerGenericReply reply = new ServerGenericReply.Builder(msg).build();
                if (reply.getResult() == ServerGenericReply.RESULT_OK) {
                    StatusActivity.addMessage("收到服务器回复：0x" + Integer.toHexString(reply.getReqId()));
                    if (AuthenticateRequest.ID == reply.getReqId()) {
                        switch (reply.getResult()) {
                            case ServerGenericReply.RESULT_OK:
                                isAuthenticate = true;
                                StatusActivity.addMessage("Auth SUCCESS!!!");
                                state = ST_AUTHED;
                                stateMerchine();
                                startHeart();
                                break;
                            case ServerGenericReply.RESULT_FAIL:
                            case ServerGenericReply.RESULT_UNSUPPORTED:
                            case ServerGenericReply.RESULT_BAD_REQUEST:
                            case ServerGenericReply.RESULT_CONFIRM:
                            default:
                                StatusActivity.addMessage("Auth FAIL!!!");
                                state = ST_ERROR;
                                stateMerchine();
                                break;
                        }
                    }
                }
                break;
            }
            case CommandReply.ID: {
                StatusActivity.addMessage("收到命令：0x" + Integer.toHexString(0xffff & msg.getId()));

                CommandReply reply = new CommandReply.Builder(msg).build();
//                EventBus.getDefault().post(reply);
                if (reply.getCommand() == 0x64) {
                    preferences.edit().putBoolean("block", true).apply();
                    Toast.makeText(context, "收到锁车命令", Toast.LENGTH_LONG).show();
                } else if (reply.getCommand() == 0x65) {
                    preferences.edit().putBoolean("block", false).apply();
                    Toast.makeText(context, "收到解锁命令", Toast.LENGTH_LONG).show();
                }
                ServerGenericRequest request = new ServerGenericRequest.Builder()
                        .flowId(reply.getSn())
                        .replyId(msg.getId())
                        .resultCode((byte) 0).build();
                sendNettyMsg(request);
                break;
            }
            default: {
                String t = "收到不支持的命令：0x" + Integer.toHexString(0xffff & msg.getId());
                Toast.makeText(context, t, Toast.LENGTH_LONG).show();
                StatusActivity.addMessage(t);
                break;
            }
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvtSendSimLoc(EvtSendSimLoc e) {
        send(e.toPosition());
    }

    private void send(final Position position) {
        log("send", position);
        lock();
//            try {
        if (isAuthenticate) {
            StatusActivity.addMessage(context.getString(R.string.status_location_update) + ":" + position.getLongitude() + "-" + position.getLatitude());
            boolean a = preferences.getBoolean("acc", false);
            boolean b = preferences.getBoolean("alarm", false);
            boolean c = preferences.getBoolean("block", false);
            SimpleDateFormat df = new SimpleDateFormat("yyMMddHHmmss");
            String date = df.format(position.getTime());
            double[] p = GPSUtil.gcj02_To_Gps84(position.getLatitude(), position.getLongitude());
            LocationMessage msg = new LocationMessage.Builder()
                    .setLatitude(p[0])
                    .setLongitude(p[1])
                    .setDirection((short) position.getCourse())
                    .setSpeed((short) position.getSpeed())
                    .setTimestamp(Long.parseLong(date))
                    .setAltitude((short) position.getAltitude())
                    .setAcc(a)
                    .setPosValid(true)
                    .setBlock(c)
                    .setPowerOff(b)
                    .build();
//                        .appendQueryParameter("bearing", String.valueOf())
//                        .appendQueryParameter("batt", String.valueOf(position.getBattery()));
            sendNettyMsg(msg);
            delete(position);
            RxBus.getInstance().post(new EvtPosSent(position));
        }
//            } catch (IllegalAccessException e) {
//                StatusActivity.addMessage(context.getString(R.string.status_send_fail));
//                retry();
//            }
        unlock();
    }

    private void retry() {
        log("retry", null);
        handler.postDelayed(() -> {
            if (isOnline) {
                read();
            }
        }, RETRY_DELAY);
    }

    private static final int ST_NONE = 0;
    private static final int ST_CONNECTED = 1;
    private static final int ST_REGIST = 2;
    private static final int ST_AUTHED = 3;
    private static final int ST_LOCATION = 4;
    private static final int ST_ERROR = 10;
    private int state = ST_NONE;
    private boolean isAuthenticate = false;

    private void stateMerchine() {
        switch (state) {
            case ST_NONE:
                MessageBuilder.phoneStr = preferences.getString(MainFragment.KEY_DEVICE, "undefined");
                startNetty();
                break;
            case ST_CONNECTED: {
                RegisterRequest request = new RegisterRequest.Builder()
                        .provId((short) 2)
                        .cityId((short) 2)
                        .setMfrsId("zGPS")
                        .cltModel("ICCID:1234567890123")
                        .plateText("川A12345")
                        .build();
                sendNettyMsg(request);
                break;
            }
            case ST_REGIST: {
                //begin auth
                if (mAuthCode == null) {
                    StatusActivity.addMessage("鉴权码为空");
                    break;
                }
                AuthenticateRequest request = new AuthenticateRequest.Builder(mAuthCode).build();
                sendNettyMsg(request);
                break;
            }
            case ST_AUTHED:
                //Now you can send other message
                read();
                StatusActivity.addMessage("开始发送存量位置");
                break;
            case ST_ERROR:
                break;
            case ST_LOCATION:
                break;
        }
    }

    private Runnable heartRun = this::heart;
    private void startHeart() {
        stopHeart();
        handler.postDelayed(heartRun, 60*1000);
//        handler.postDelayed(heartRun, 10*1000);
    }
    private void heart() {
        StatusActivity.addMessage("发送心跳数据");
        HeartbeatMessage msg = new HeartbeatMessage.Builder().build();
        sendNettyMsg(msg);
    }

    private void stopHeart() {
        handler.removeCallbacks(heartRun);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(EvtStartPos e) {
        startSendLocation();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(EvtStopPos e) {
        positionProvider.stopUpdates();
    }

    private void startSendLocation() {
        try {
            positionProvider.startUpdates();
        } catch (SecurityException e) {
            Log.w(TAG, e);
        }
//        state = ST_LOCATION;
//        stateMerchine();
    }

    private void sendPos(Position position) {
        if (buffer) {
            write(position);
        } else {
            send(position);
        }
    }

    private Channel channel;
    private EventLoopGroup group = new NioEventLoopGroup();

    private void startNetty() {
        Bootstrap bootstrap = new Bootstrap().group(group);
        //禁用nagle算法 Nagle算法就是为了尽可能发送大块数据，避免网络中充斥着许多小数据块。
        bootstrap.option(ChannelOption.TCP_NODELAY, true)//屏蔽Nagle算法试图
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
        //指定NIO方式   //指定是NioSocketChannel, 用NioSctpChannel会抛异常
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
//                socketChannel.pipeline().addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));//5s未发送数据，回调userEventTriggered
                socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                        super.channelInactive(ctx);
                        handler.post(() -> StatusActivity.addMessage("已关闭连接"));
                        checkReconn();
                    }
                });
                socketChannel.pipeline().addLast(new FrameDecoderHandler());
//                socketChannel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
//                socketChannel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                socketChannel.pipeline().addLast(new MsgInHandler());
                socketChannel.pipeline().addLast(new MsgOutHandler());
//                socketChannel.pipeline().addLast(new HeartbeatServerHandler());
//                                Packet packet = Packet.newInstance();
//                                byte[] bytes = packet.packetHeader(20, 0x100, (short) 200, (short) 0, (short) 1, 1, 0);
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(mHost, mPort)
                .addListener((ChannelFutureListener) future -> {
                    if (future.isSuccess()) {
                        //连接成功
                        channel = future.channel();
                        RxBus.getInstance().post(new EvtIpConnected(true));

                        handler.post(() -> StatusActivity.addMessage("连接成功"));
                        state = ST_CONNECTED;
                        stateMerchine();
                    } else {
                        //连接失败
//                        RxBus.getInstance().post(new EvtIpConnected(false));
                        handler.post(() -> StatusActivity.addMessage("连接失败:" + future.cause().getMessage()));
                        future.channel().close();
//                        group.shutdownGracefully();
                        checkReconn();
                    }
                });
    }

    private void checkReconn() {
        if (preferences.getBoolean(MainFragment.KEY_STATUS, false)) {
            handler.post(() -> StatusActivity.addMessage("10S后重连"));
            handler.postDelayed(reconnRun, 10 * 1000);
        }
    }

    private Runnable reconnRun = this::startNetty;

    private void stopNetty() {
        if (channel != null) {
            channel.close().awaitUninterruptibly();
        }
//        group.shutdownGracefully().awaitUninterruptibly();
    }

    private void sendNettyMsg(Message message) {
        if (isAuthenticate) {
            startHeart();
        }
        for (Packet packet : message.getPackets()) {
            byte[] b = packet.getBytes();
            Log.d(TAG, "writePackets 2: " + ArrayUtils.bytesToHexFun3(b));
            if (channel.isActive()) {
                channel.writeAndFlush(b);
            }
        }
    }

}
